# -*- coding: utf-8 -*-

# @File   : json
# @Author : Yupeng
# @Date   : 2018/111/17

import json
from apps.search.utils.serilizers.common import *
from apps.storage.models.template import TemplateField, TemplateFieldEnum, TemplateField_lyp
from apps.storage.models.data import *
from mongoengine import DoesNotExist
from django.utils.translation import ugettext as _
from django.conf import settings
from django.utils import timezone


class JSONFieldSerializer(FieldSerializer):

    def __init__(self, absolute_file_path=False, decompose_range=True):
        self._absolute_file_path = absolute_file_path
        self._decompose_range = decompose_range

    def serialize_field(self, mongo_value, field_meta):
        if mongo_value is None:
            return None
        else:
            return super().serialize_field(mongo_value, field_meta)

    def serialize_number(self, mongo_value, field_meta):
        if field_meta.unit is not None:
            return mongo_value
        return mongo_value

    def serialize_range(self, mongo_value, field_meta):
        field_type = field_meta.field_type
        if field_type.is_range_interval(field_meta.range_type):
            lb = mongo_value.get('lb', '-∞')
            ub = mongo_value.get('ub', '+∞')
            if self._decompose_range:
                return {'lb': lb, 'ub': ub}
            return f'({lb}, {ub})'
        else:
            err = mongo_value.get('err', '?')
            val = mongo_value.get('val', '?')
            if self._decompose_range:
                return {'val': val, 'err': err}
            return f'{val}±{err}'

    def serialize_image(self, mongo_value, field_meta):
        return self.serialize_file(mongo_value, field_meta)

    def serialize_file(self, mongo_value, field_meta):
        if self._absolute_file_path:
            return [settings.SITE_ADDR + settings.MEDIA_URL + x for x in mongo_value]
        return [settings.MEDIA_URL + x for x in mongo_value]

    def serialize_choice(self, mongo_value, field_meta):
        return str(mongo_value)


class FlatJSONFieldSerializer(JSONFieldSerializer):

    def serialize_image(self, mongo_value, field_meta):
        return ' '.join(super().serialize_image(mongo_value, field_meta))

    def serialize_file(self, mongo_value, field_meta):
        return ' '.join(super().serialize_file(mongo_value, field_meta))


class JSONFieldParser(FieldParser):

    def parse_field(self, field_meta: TemplateField, raw_value):
        if raw_value is None:
            if field_meta.required:
                raise ParsingError(ParsingErrorEnum.VALUE_MISSING, o1=field_meta.field_path_str)
            return None
        else:
            return super().parse_field(field_meta, raw_value)

    def parse_string(self, field_meta, raw_value: str):
        if not isinstance(raw_value, str):
            raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(raw_value), o2=field_meta.field_type.description, o3=field_meta.external_field_path_str)
        if raw_value.strip() == '':
            if field_meta.required:
                raise ParsingError(ParsingErrorEnum.VALUE_MISSING, o1=field_meta.field_path_str)
            return None
        return raw_value

    def parse_number(self, field_meta, raw_value: float):
        try:
            return float(raw_value)
        except (TypeError, ValueError):
            raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(raw_value), o2=field_meta.field_type.description, o3=field_meta.external_field_path_str)

    def parse_range(self, field_meta, raw_value: str):
        if TemplateFieldEnum.is_range_error(field_meta.range_type):
            try:
                err_data = raw_value.split('±')
                val = float(err_data[0])
                err = float(err_data[1])
                return {'val': val, 'err': err}
            except (IndexError, AttributeError, TypeError, ValueError):
                raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(raw_value),
                                   o2=_("Error Type"), o3=field_meta.external_field_path_str)
        else:
            try:
                trimmed = raw_value[1:-1]
                bounds = trimmed.split(',')
                return {'lb': float(bounds[0]), 'ub': float(bounds[1])}
            except (IndexError, AttributeError, TypeError, ValueError):
                raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(raw_value),
                                   o2=_("Interval Type"), o3=field_meta.external_field_path_str)

    def parse_file_image(self, field_meta, raw_value: str):
        path_list = raw_value
        url_list = []
        for path in path_list:
            url = self.parse_single_file_or_image(field_meta, path)
            if url is not None:
                url_list.append(url)
        if len(url_list) == 0:
            if field_meta.required:
                raise ParsingError(ParsingErrorEnum.VALUE_MISSING, o1=field_meta.field_path_str)
        elif len(url_list) > 1 and not field_meta.has_multiple_files:
            raise ParsingError(ParsingErrorEnum.EXCESSIVE_FILES_OR_IMAGES, o1=field_meta.field_path_str)
        return url_list

    def parse_choice(self, field_meta, raw_value: str):
        choices = field_meta.choices_list
        if raw_value not in choices:
            raise ParsingError(ParsingErrorEnum.INVALID_CHOICE_VALUE, o1=raw_value,
                               o2=', '.join(choices))
        return raw_value


class JSONHandler(DataHandler):
    '''
    这个类是干嘛的
    给模板，然后将模板拍平。
    '''

    def __init__(self, mode: DataMode, template: Template = None):
        super().__init__(mode, template)
        self._content_to_save = None
        self._template = template
        if mode == DataMode.WRITE or mode == DataMode.TEMPLATE:
            if template is None:
                raise ValueError(f'A template must be provided in {self._mode.description} mode.')
            self._load_template(template)

    def _load_template(self, template: Template):
        self._content_to_save = {'template': template.dict_for_export, 'data': []}

    def _meta_to_dict(self, data_meta, with_none):
        meta_body = {}
        for field in DataMetaFieldEnum.writer_iterator(with_id=True):
            value = field.get_value(data_meta)
            if (value is None or getattr(value, '__len__', int)() == 0) and not with_none:
                continue
            meta_body[field.description] = value
        return meta_body

    def _data_to_dict(self, data_meta: DataMeta, with_none, absolute_file_path, decompose_range):

        serializer = JSONFieldSerializer(absolute_file_path=absolute_file_path, decompose_range=decompose_range)

        def _add_field(field_meta: TemplateField_lyp, mongo_value):
            if mongo_value is None:
                return None
            t = field_meta.field_type
            if t.is_table:
                values = []
                rows = DataFieldTableRow.objects(_owner_id=mongo_value)
                for row in rows:
                    row_value = {}
                    for header in field_meta.sub_fields:
                        v = _add_field(header, getattr(row, header.field_name, None))
                        if v is not None or with_none:
                            row_value[header.field_name] = v
                    if row_value:
                        values.append(row_value)
            elif t.is_container or t.is_generator:
                values = {}
                try:
                    container_content = DataFieldContainer.objects.get(pk=mongo_value)
                except DoesNotExist:
                    container_content = {}
                for sub_field in field_meta.sub_fields:
                    if sub_field.field_name in container_content:
                        v = _add_field(sub_field, getattr(container_content, sub_field.field_name, None))
                        if v is not None or with_none:
                            values[sub_field.field_name] = v
            elif t.is_array:
                values = []
                for element_value in mongo_value:
                    v = _add_field(field_meta.element_field, element_value)
                    if v is not None:
                        values.append(v)
            else:
                values = serializer.serialize_field(mongo_value, field_meta)
            return values

        try:
            content = DataContent.objects.get(pk=data_meta.dc_id)
        except DoesNotExist:
            content = {}  # TODO 找不到

        content_body = {}

        for field in self._template.fields:
            v = _add_field(field, getattr(content, str(field.field_name), None))
            if v is not None or with_none:
                content_body[field.field_name] = v
        return content_body

    def write_data(self, data_meta: DataMeta, with_none=False):
        if self._mode != DataMode.WRITE:
            raise ValueError(f"Cannot write XML in {self._mode.description} mode.")
        if not isinstance(data_meta, DataMeta):
            raise ValueError("data_meta should be an instance of DataMeta")
        if data_meta.tid != self._template.id:
            raise ValueError(f'Data\'s tid should be "{self._template.id}", got "{data_meta.tid}" instead')

        meta_body = self._meta_to_dict(data_meta, with_none)
        content_body = self._data_to_dict(data_meta, with_none, absolute_file_path=True, decompose_range=False)

        self._content_to_save['data'].append({'meta': meta_body, 'content': content_body})

    def read_data(self, path_or_fp, uploaded_by: str, file_dir=None, progress_handler: Callable[[float], None] = None):
        if self._mode != DataMode.READ:
            raise ValueError(f"Cannot read XML in {self._mode.description} mode.")

        parser = JSONFieldParser(uploaded_by, file_dir)
        current_path_str = ""
        total_count = 0
        current_count = 0

        def _read_field(field_meta: TemplateField_lyp, field_value):
            """
            读取某个字段的值
            :param field_meta: 字段meta
            :return: 字段的json值
            """
            nonlocal current_path_str
            current_path_str = field_meta.external_field_path_str
            t = field_meta.field_type
            if field_value is None:
                if field_meta.required:
                    raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1=current_path_str)
                return None

            if t.is_array or t.is_table or t.is_file or t.is_image:
                if not isinstance(field_value, list):
                    raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(field_value), o2=field_meta.field_type.description, o3=field_meta.external_field_path_str)
                if len(field_value) == 0:
                    if field_meta.required:
                        raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1=current_path_str)

            elif t.is_container or t.is_generator:
                if not isinstance(field_value, dict):
                    raise ParsingError(ParsingErrorEnum.INVALID_VALUE, o1=str(field_value), o2=field_meta.field_type.description, o3=field_meta.external_field_path_str)
                if len(field_value) == 0:
                    if field_meta.required:
                        raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1=current_path_str)

            if t.is_container:
                raw_value = {}
                for sub_field in field_meta.sub_fields:
                    raw_value[sub_field.field_name] = _read_field(sub_field, field_value.get(sub_field.field_name))
            elif t.is_array:
                raw_value = []
                for raw_element in field_value:
                    v = _read_field(field_meta.element_field, raw_element)
                    if v is not None:
                        raw_value.append(v)
            elif t.is_table:
                raw_value = []
                for row in field_value:
                    raw_value.append({})
                    for header in field_meta.sub_fields:
                        raw_value[-1][header.field_name] = _read_field(header, row.get(header.field_name))
            elif t.is_generator:
                selected_field = list(field_value.keys())[0]
                v = _read_field(field_meta[selected_field], field_value)
                if v is not None:
                    raw_value = {selected_field: v}
                else:
                    raw_value = None
            else:
                raw_value = parser.parse_field(field_meta, field_value)
            return raw_value

        def _read_data(data_json):
            nonlocal current_count, total_count
            data = {}
            for field in template.fields:
                data[field.field_name] = _read_field(field, data_json.get(field.field_name))
            current_count += 1
            if progress_handler:
                progress_handler(current_count / total_count)
            return data

        def _read_meta(meta_json):
            temp_meta = {}
            for meta_field in DataMetaFieldEnum.reader_iterator():
                temp_meta[meta_field.field_raw_name] = meta_field.get_value_from_dict(meta_json, translated_key=True)
            temp_meta.pop(None, None)
            return temp_meta

        try:
            if isinstance(path_or_fp, str):
                with open(path_or_fp, encoding='utf-8') as fp:
                    json_dict = json.load(fp)
            else:
                json_dict = json.load(path_or_fp)
        except json.JSONDecodeError as e:
            raise ParsingError(ParsingErrorEnum.JSONSyntaxError, o1=str(e))
        tid = json_dict.get('template', {}).get('_id')
        if tid is None:
            raise ParsingError(ParsingErrorEnum.BAD_TEMPLATE)
        try:
            template = Template.objects.get(id=tid)
            self._load_template(template)
        except Template.DoesNotExist:
            raise ParsingError(ParsingErrorEnum.TEMPLATE_DOES_NOT_EXIST, o1=tid)

        if 'data' not in json_dict or not isinstance(json_dict['data'], list):
            raise ParsingError(ParsingErrorEnum.BAD_TEMPLATE)
        data_list = json_dict['data']
        total_count = len(data_list) * 2  # 认为数据解析和写数据库各占一半时间
        current_count = 0
        try:
            for data_json in data_list:
                meta = _read_meta(data_json.get('meta'))
                if meta is None:
                    raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1='meta')
                data = _read_data(data_json.get('content'))
                if data is None:
                    raise ParsingError(ParsingErrorEnum.FIELD_MISSING, o1='content')
                self.meta_id_list.append(template.add_data(meta, data, uploaded_by=uploaded_by, importing=True).id)
                current_count += 1
                if progress_handler:
                    progress_handler(current_count / total_count)
            return self.meta_id_list
        except Exception:
            self.roll_back()
            raise

    def generate_template(self, number_of_data=2, elements_per_array=2, rows_per_table=2):
        """
        生成导入模板
        :param number_of_data:
        :param elements_per_array:
        :param rows_per_table:
        :return:
        """

        def _add_field(field_meta: TemplateField_lyp):
            t = field_meta.field_type
            if t.is_table:
                data = []
                for index in range(1, rows_per_table + 1):
                    data.append({})
                    for header in field_meta.sub_fields:
                        data[-1][header.field_name] = _add_field(header)
            elif t.is_generator:
                data = {}
                for option in field_meta.sub_fields:
                    data[option.field_name] = (_add_field(option))
            elif t.is_container:
                data = {}
                for sub_field in field_meta.sub_fields:
                    data[sub_field.field_name] = _add_field(sub_field)
            elif t.is_array:
                data = []
                for index in range(1, elements_per_array + 1):
                    data.append(_add_field(field_meta.element_field))
            elif t.is_file or t.is_image:
                data = []
                link_node_name = 'image' if t.is_image else 'file'
                for index in range(1, elements_per_array + 1):
                    data.append(_("***Fill %(type_name)s %(file_number)s's path here***") %
                                {
                                    'type_name': link_node_name,
                                    'file_number': str(index)
                                }
                                )
            else:
                context = dict(field_name=field_meta.field_name, field_type=t.description)
                if t.is_choice:
                    context['choices'] = ', '.join(field_meta.choices_list)
                    data = _(
                        '***Fill %(field_name)s (%(field_type)s) here. Choices are %(choices)s ***') % context
                else:
                    data = _('***Fill %(field_name)s (%(field_type)s) here***') % context

            return data

        for index in range(1, number_of_data + 1):
            meta_part = {}
            content_part = {}
            for field in DataMetaFieldEnum.reader_iterator(with_id=True):
                if field == DataMetaFieldEnum.DATA_ID:
                    text = _("***Data %(data_number)s's ID***") % {'data_number': index}
                else:
                    text = _('***Fill %(field_name)s here***') % {'field_name': field.description}
                meta_part[field.description] = text

            for field in self._template.fields:
                content_part[field.field_name] = _add_field(field)
            self._content_to_save['data'].append({'meta': meta_part, 'content': content_part})

    def save(self, fp_or_path):
        if self._mode == DataMode.READ:
            raise ValueError("Nothing to save in read mode.")
        content_to_save = json.dumps(self._content_to_save, ensure_ascii=False, indent=4).encode('utf-8')
        if hasattr(fp_or_path, 'write'):
            fp_or_path.name = f'{self._template.title}.{timezone.now().strftime("%Y%m%d%H%M%S")}.json'
            fp_or_path.write(content_to_save)
            fp_or_path.seek(0)
            return fp_or_path.name
        else:
            with open(fp_or_path, 'wb', encoding='utf-8') as fp:
                fp.write(content_to_save)
            return os.path.basename(fp_or_path)

    def data_to_dict(self, data_meta: DataMeta, absolute_file_path=False, decompose_range=True):
        return self._data_to_dict(data_meta, with_none=False, absolute_file_path=absolute_file_path,
                                  decompose_range=decompose_range)
